'use strict'

const crypto = require('crypto')

let _randstrList = [
  'a', 'b', 'c', 'd', 'e', 'f', 'g',
  'h', 'i', 'j', 'k', 'l', 'm', 'n',
  'o', 'p', 'q', 'r', 's', 't', 'u',
  'v', 'w', 'x', 'y', 'z',

  '1', '2', '3', '4', '5', '6', '7', '8', '9'
]

function randstring(length = 8) {
  let rstr = ''
  let ind = 0

  for (let i = 0; i < length; i++) {
    ind = parseInt(Math.random() * _randstrList.length)
    rstr += _randstrList[ind]
  }
  return rstr
}

class TitbitToken {

  constructor(options = {}) {

    //sm4-cbc or aes-256-cbc 
    //openssl list -cipher-algorithms
    this.algorithm = 'sm4-cbc'
    
    this.iv = randstring(16)

    this.key = randstring(16)

    this.tokenEncoding = 'base64url'

    //默认3小时有效
    this.expires = 10800000

    this.refresh = 0

    this.failedCode = 401

    /**
     * tokenId用于识别token是否有效，如果发现token存在泄漏的可能，则在服务运行时，即可更改此值。
     * 此时，再次验证token则会失效。
     * */

    this.tokenMap = new Map()

    this.idKeyIV = new Map()

    this.tokenIds = []

    this.idIndex = -1

    this.tokenTag = 'titbit-token'
    this.tokenStartIndex = this.tokenTag.length + 1

    if (typeof options !== 'object') {
      options = {}
    }

    for (let k in options) {
      switch (k) {
        case 'iv':
          this.iv = options[k]
          break
          
        case 'key':
          this.key = options[k]
          break

        case 'expires':
          if (typeof options[k] === 'number' && options[k] > 0) {
            this.expires = parseInt(options[k] * 1000)
          }
          break
        
        case 'encoding':
          this.tokenEncoding = options[k]
          break

        case 'alg':
        case 'algorithm':
          if (['aes-128-cbc', 'aes-192-cbc', 'aes-256-cbc', 'sm4-cbc', 'sm4'].indexOf(options[k].toLowerCase()) >= 0)
            this.algorithm = options[k].toLowerCase()
          if (this.algorithm === 'sm4') {
            this.algorithm = 'sm4-cbc'
          }

        case 'failedCode':
          if (typeof options[k] === 'number' && options[k] >= 400 && options[k] <= 499) {
            this.failedCode = options[k]
          }
          break

      }
    }

    this.key = this.fixKey(this.key)
    this.iv = this.fixIv(this.iv)

  }

  _aesEncrypt(data, key, iv, options = {}) {
    var h = crypto.createCipheriv(this.algorithm, key, iv, options)
    let hd = h.update(data, 'utf8', this.tokenEncoding)
    hd += h.final(this.tokenEncoding)
    return hd
  }

  _aesDecrypt(data, key, iv, options = {}) {
    var h = crypto.createDecipheriv(this.algorithm, key, iv, options)
    let hd = h.update(data, this.tokenEncoding, 'utf8')
    hd += h.final('utf8')
    return hd
  }

  addTokenId(tid) {
    if (typeof tid === 'string') {
      tid = [ tid ]
    }

    if (tid.toString() === '[object Object]') {

      for (let k in tid) {
        if (this.tokenIds.indexOf(k) < 0) {
          this.tokenIds.push(k)
          this.setMapId(k, tid[k])
        }
      }

    } else if (tid instanceof Array) {

      for (let i=0; i < tid.length; i++) {
        if (this.tokenIds.indexOf(tid[i]) < 0) {
          this.tokenIds.push(tid[i])
          this.tokenMap.set(tid[i], this.tokenIds.length)
        }
      }

    }

  }

  setMapId(tid, iv = null) {
    this.tokenMap.set(tid, iv || tid)
  }

  fixKey(key) {
    let leng = key.length
    let real_leng = 32
    if (this.algorithm.indexOf('sm4') === 0 || this.algorithm.indexOf('aes-128') === 0) {
      real_leng = 16
    } else if (this.algorithm.indexOf('aes-192') === 0) {
      real_leng = 24
    } else if (this.algorithm.indexOf('aes-256') === 0) {
      real_leng = 32
    }

    if (leng === real_leng) return key;
    if (leng > real_leng) return key.substring(0, real_leng);
    let arr = [];
    for (let i = real_leng - leng; i > 0; i--) {
      arr.push('a');
    }

    return key + arr.join('');
  }

  fixIv(iv) {
    let leng = iv.length
    if (leng === 16) return iv;
    if (leng > 16) return iv.substring(0, 16);
    let arr = [];
    for (let i = 16 - leng; i > 0; i--) {
      arr.push('1');
    }
    return iv + arr.join('');
  }

  /**
   * 
   * @param {string} id 
   * @param {string} key 
   * @param {string} id 
   */
  setIdKeyIv(id, key, iv) {
    key = this.fixKey(key);
    iv = this.fixIv(iv);
    
    this.addTokenId(id)

    this.idKeyIV.set(id, {
      key : key,
      iv : iv,
      id : id,
    })
  }

  hasId(tid) {
    return this.tokenMap.has(tid)
  }

  /**
   * 允许tokenMap和tokenIds不一致，这种情况是针对以下需求设计：
   *    验证token是有效的，但是却只针对某些用户签发，对其他用户是不会签发的，
   *    签发是通过make传递参数指定的。
   * 
   * @param {string} tid 
   */
  removeTokenId(tid) {
    let ind = this.tokenIds.indexOf(tid)

    if (ind >= 0) {
      this.tokenIds.splice(ind, 1)
    }

    this.tokenMap.delete(tid)
    this.idKeyIV.delete(tid)
  }

  randId() {
    if (this.tokenIds.length <= 0) {
      return ''
    }

    let ind = parseInt( Math.random() * this.tokenIds.length)

    return this.tokenIds[ind]
  }

  stepId() {
    if (this.tokenIds.length <= 0) {
      return ''
    }

    if (this.idIndex >= this.tokenIds.length - 1) {
      this.idIndex = -1
    }

    this.idIndex += 1

    return this.tokenIds[this.idIndex]
  }

  setIv(iv) {
    this.iv = this.fixIv(iv)
  }

  setKey(key) {
    this.key = this.fixKey(key)
  }

  setEncoding(ecode) {
    this.tokenEncoding = ecode
  }

  setExpires(expires) {
    this.expires = expires
  }

  setRefresh(flag = true) {
    if (flag) {
      this.refresh = parseInt(this.expires / 5)
    } else {
      this.refresh = 0
    }
  }

  refreshToken(t, ikv = null) {
    if (t.data.expires + t.data.timestamp - t.now < this.refresh) {
      if (ikv && typeof ikv === 'object') {
        return this.makeikv(t.data, ikv)
      }
      return this.make(t.data, t.data.__tokenId__)
    }
    return null
  }

  /**
   * @param {object} userinfo 
   */
  make(userinfo, tokenId = null) {

    if (!userinfo.expires || typeof userinfo.expires !== 'number') {
      userinfo.expires = this.expires
    }

    userinfo.timestamp = Date.now()

    userinfo.__tokenId__ = tokenId || this.stepId()

    let tk

    let ikv = tokenId ? this.idKeyIV.get(tokenId) : null

    if (tokenId && ikv) {
      tk = this._aesEncrypt(JSON.stringify(userinfo), ikv.key, ikv.iv)
    } else {
      tk = this._aesEncrypt(JSON.stringify(userinfo), this.key, this.iv)
    }

    return tk
  }

  randIvToken (info, id = null, key = null) {
    let riv = randstring(16)
    let tid = id || this.randId()
    let opts = {
      id : tid,
      key : key || this.key,
      iv : riv
    }

    opts.token = this.makeikv(info, opts)
    
    return opts
  }

  makeAccessToken(info, id=null, key=null) {
    let riv = randstring(16)
    let tid = id || this.randId()
    let token = this.makeikv(info, {id: tid, iv: riv, key: key||this.key})
    return `${this.tokenTag} ${riv}.${token}`
  }

  verifyAccessToken(edata) {
    let ivtoken = edata.substring(this.tokenStartIndex)
    let tarr = ivtoken.split('.')

    if (tarr.length != 2 || !tarr[0] || !tarr[1]) {
      return {
        ok: false,
        errcode: 'ILLEGAL'
      }
    }

    return this.verify(tarr[1], {iv: tarr[0]})
  }

  makeikv(userinfo, ikv) {
    if (!userinfo.expires || typeof userinfo.expires !== 'number') {
      userinfo.expires = this.expires
    }

    userinfo.timestamp = Date.now()
    userinfo.__tokenId__ = ikv.id

    return this._aesEncrypt(JSON.stringify(userinfo), ikv.key, ikv.iv)
  }

  verifyikv(edata, ikv) {
    return this.verify(edata, ikv)
  }

  verifyid(edata, tid) {
    let tk = this.idKeyIV.get(tid)
    if (tk) {
      return this.verify(edata, tk)
    }
    return this.verify(edata)
  }

  verify(edata, ikv={}) {
    try {
      let u = this._aesDecrypt(edata, ikv.key || this.key, ikv.iv || this.iv)
      let uj = JSON.parse(u)
      let tm = Date.now()

      if (uj.timestamp + uj.expires < tm) {
        return {
          ok : false,
          errcode : 'TIMEOUT'
        }
      }

      if (uj.__tokenId__
        && ( (ikv.id && uj.__tokenId__ !== ikv.id) || !this.tokenMap.has(uj.__tokenId__)) )
      {
        return {
          ok : false,
          errcode : 'ILLEGAL'
        }
      }

      return {
        ok : true,
        data : uj,
        now : tm
      }

    } catch (err) {
      return {
        ok : false,
        errcode : 'FAILED'
      }
    }

  }

  mid() {
    let self = this

    return async (c, next) => {
      let token = c.headers.authorization

      if (!token) {
        return c.status(self.failedCode).send('!token')
      }

      let uinfo = self.verify(token)

      if (!uinfo.ok) {
        return c.status(self.failedCode).send(uinfo.errcode)
      }

      c.box.user = uinfo

      if (uinfo.data.expires + uinfo.data.timestamp - uinfo.now < self.refresh) {
        let new_token = self.make(uinfo.data, uinfo.data.__tokenId__)
        c.setHeader('x-refresh-token', new_token)
      }

      await next()
    }
  }

}

module.exports = TitbitToken

